C++ Primer 第3章 字符串、向量和数组

3.1 命名空间的using声明

作用域操作符(::)的含义:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。

通过使用using声明,可以简单的使用到命名空间中的成员。有了using声明就无须专门的前缀也能使用所需的名字了。

按照规定,每个using 声明引入命名空间中的一个成员。每个用到的i那个字都必须有自己的声明语句,而且每句话都以分号结束。

位于头文件的代码一般来说不应该使用using声明,这是为了防止产生始料未及的名字冲突。

3.2 标准库类型string

标准库类型string 表示可变长的字符序列,使用string类型必须首先包含string头文件,string定义在命名空间std中。

3.2.1 定义和初始化string对象

初始化string对象常用的方式:

string s1;    //默认初始化,s1是一个空字符串
string s2 = s1;    //s2是s1的副本
string s2(s1);    //s2是s1的副本
string s3("hiya");    //s3是字面值”hiya“的副本,除了字符串最后的那个空字符外
string s3 = "hiya";    //s3是字符串字面值的副本
string s4(n, 'c');    //s4的内容是n个字符c组成的串

拷贝初始化:如果使用等会(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。

直接初始化:如果不使用等号,则执行的是直接初始化。

当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像s4那样初始化要用到的值有多个,一般只能使用直接初始化:

string s5 = "hiya";        //拷贝初始化
string s6("hiya");        //直接初始化
string s7(10, 'c');        //直接初始化,s7的内容是cccccccccc

对于多个值进行初始化,也可以使用拷贝初始化,不过需要显示创建一个(临时)对象用于拷贝:

string s8 = string(10,'c');

3.2.2 string对象上的操作

string的操作操作说明
os << s将s写到输出流os当中,返回os
is >> s从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is, s)从is中读取一行赋给s,返回is
s.empty()s为空返回true,否则返回false
s.size()返回s中字符的个数
s[n]返回s中第n个字符的引用,位置n从0算起
s1 + s2返回s1和s2连接后的结果
s1 = s2用s2的副本代替s1中原本的字符
s1 == s2如果s1和s2中所含的字符完全一样,则它们相等;
s1 != s2string对象的相等性判断对字母的大小写敏感
<,<=,>,>=利用字符在字典中的顺序进行比较,对大小写敏感

string对象的输入以第一个非空白(空格符、换行符、制表符等)字符读起,直至遇到下一处空白为止。

string对象的输入输出操作返回运算符左侧的运算对象作为其结果,因此,可以多个输入或输出连写在一起。

想要保留输入时的空白符,应该使用getline函数,函数从给定的输入流中读取内容,直到遇到换行符为止(换行符也被读进来了),但是所读的内容存入到string对象时,换行符并未存入到srting对象中,而是被丢弃了。

size函数返回一个string::size_type类型:它是一个无符号类型的值,足够存放下任何string对象的大小。

string类及其他大多数标准库类型都定义了几种配套类型,这些配套类型体现了标准库类型与机器无关的特性。

C++11新标准中,允许编译器通过auto或者decltype来推断变量类型:

auto len = line.size();    //len的类型是string::size_type

如果一条表达式已有size()函数就不要使用int 了,可以避免混用int 和 unsigned可能带来的问题。

相等性运算符分别检验两个string对象相等或不相等,string对象相等意味着它们的长度相同而且所包含的字符也全相同。

关系运算符<,<=,>,>=:

  1. 如果两个string对象长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,则较短string小于较长string
  2. 如果两个string对象在某些对应的位置上不一样,则string对象的比较结果其实是string对象中第一对相异字符比较的结果

两个string对象相加,得到的是一个新的string对象,其包含的字符由两部分组成:前半部分是加号左侧string对象所含字符,后半部分是加号右侧string对象所含字符。

复合赋值运算符(+=)则是将右侧string对象的内容追加到左侧string对象的后面。

当把string对象和字符字面值以及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)两侧的运算对象至少有一个是string类型

string s4 = s1 + ", ";            //正确:把一个string对象和一个字面值相加
string s5 = "hello" + ", ";        //错误:两个运算对象都不是string

string s6 = s1 + ", " + "world";//正确:每个加法运算符都有一个运算对象是string
string s6 = (s1 + ", ") + "world";    //上面的等价
string s7 = "hello" + ", " + "world";    //错误:不能把字面值直接相加

标准库允许把字符字面值和字符串字面值转换成string对象。因为历史原因,也为了与C兼容,所以C++语言的字符串字面值并不是标准库类型string的对象,字符串字面值与string是不同的类型。

3.2.3 处理string对象中的字符

cctype头文件中的函数函数说明
isalnum(c)当c是字母或数字时为真
isalpha(c)当c是字母时为真
iscntrl(c)当c是控制字符时为真
isdigit(c)当c是数字时为真
isgraph(c)当c不是空格但可以打印时为真
islower(c)当c是小写字母时为真
isprint(c)当c是可打印字符时为真(即c是空格或c具有可视形式)
ispunct(c)当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种)
isspace(c)当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种)
isupper(c)当c是大写字母时为真
isxdigit(c)当c是十六进制数字时为真
tolower(c)如果c是大写字母,输出对应小写字母,否则原样输出c
toupper(c)如果c是小写字母,输出对应大写字母,否则原样输出c

C++标准库兼容了C语言的标准库,C语言的头文件形如name.h,C++则将这些文件命名为cname。在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中则不然。

C++11新标准提供了一种语法:范围for语句。这种语句遍历给定序列中的每个元素并对序列中每个值执行某种操作,形式是:

for (declaration : expression)
    statement

如果想对string 对象中的每个字符做点什么操作,这是最好的办法,如:

string str("some string");
for (auto c : str)    //只读,不可修改
    cout << c << endl;
    
for (auto &c : str)
    c = toupper(c);
cout << s << endl;

访问string对象中的单个字符有两种方式:

  1. 使用下标

    下标运算符([])接受的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用。

    下标从0计起,必须大于等于0,小于s.size()。超出范围的下标会越界。

    下标的值称作”下标“或”索引“,任何表达式只要它的值是一个整型值就能作为索引,如果某个索引是带符号类型的值,将自动转换成string::size_type表达的无符号类型。

    在访问指定字符之前,首先要检查string对象s是否为空,如果s为空,则s[0]的结果将是未定义的。

    只要字符串不是常量,就能为下标运算符返回的字符赋新值。

    string s("some string");
    if (!s.empty())
        s[0] = toupper(s[0]);
        
    for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index)
        s[index] = toupper(s[index]);
    

    逻辑与运算符(&&),如果参与运算的两个运算对象都为真,则逻辑与结果为真;否则结果为假。C++语言规定,只有当左侧运算对象为真时才会检查右侧运算对象的情况。

    使用下标可进行随机访问,但是下标必须合法。

  2. 使用迭代器

3.3 标准库类型vector

#include <vector>
using std::vector;

vector是一个类模板,C++语言既有类模板,也有函数模板。模板本身不是类或对象,编译器根据模板创建类或函数的过程称为实例化。

vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如:vector

组成vector的元素可以是vector,C++11新标准对此的定义有所改善:

//过去,右尖括号不能连在一起写,要用空格分开
vector<vector<int> > a;
//C++11
vector<vector<int>> a;

3.3.1 定义和初始化vector对象

初始化vector对象的方法方法说明
vector v1v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector v2(v1)v2中包含v1所有元素的副本
vector v2 = v1等价于v2(v1),v2中包含v1所有元素的副本
vector v3(n, val)v3包含了n个重复的元素,每个元素的值都是val
vector v4(n)v4包含了n个重复地执行了值初始化的对象
vector v5{a, b, c...}v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector v5 = {a, b, c...}等价于v5{a, b, c...}

C++11新标准还提供了列表初始化。用花括号括起来的0个或多个初始元素值被赋给vector对象:

vector<string> articles = {"a", "an", "the"};

通常情况下,可以只提供vector对象容纳的元素数量而不用略去初始值。此时库会创建一个值初始化的元素初值,该值由元素类型决定:

  • 元素是内置类型,使用默认值
  • 元素是类类型,元素由类默认初始化
  • 如果元素的类型不支持默认初始化,则必须明确提供初始值
  • 只提供元素数量而没有设定初始值,只能使用直接初始化
vector<int> v1(10);        //v1有10个元素,每个元素的值都是0
vector<int> v2{10};        //v2有1个元素:10

vector<int> v3(10, 1);    //v3有10个元素,每个元素的值都是1
vector<int> v4{10, 1};    //v4有2个元素:10和1

圆括号提供的值用来构造vector对象;花括号的值作为元素的值,执行列表初始化。

如果初始化使用花括号但是提供的值又不能用来列表初始化,就会考虑用这样的值来构造对象。

vector<string> v5{"hi"};    //列表初始化,v5有1个元素
vector<string> v6("hi");    //错误:不能用字符串字面值构建vector对象
vector<string> v7{10};        //v7有10个默认初始化的元素
vector<string> v8{10, "hi"};//v8有10个值为“hi”的元素

3.3.2 向vector对象中添加元素

如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环,范围for循环语句体内不应该改变其所遍历的序列的大小。

3.3.3 其他vector操作

vector支持的操作操作说明
v.empty()如果v不包含任何元素,返回真,否则返回假
v.size()返回v中元素的个数
v.push_back(t)向v的尾端添加一个值为t的元素
v[n]返回v中第n个位置上的元素引用
v1 = v2用v2中元素的拷贝替换v1中的元素
v1 = {a, b, c...}用列表中元素的拷贝替换v1中的元素
v1 == v2v1和v2相等,当且仅当它们的元素数量相同,且对应位置的元素值都相同
v1 != v2
<,<=,>,>=以字典顺序进行比较

size函数返回vector对象中元素的个数,返回值类型是由vector定义的size_type类型。

要使用size_type,需要首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:

vector<int>::size_type        //正确
vector::size_type            //错误

vector各个相等性运算符和关系运算符和string的相应运算符功能一致。只有当元素的值可比较时,vector对象才能被比较。

使用下标访问对象元素时,必须确认下标合法,而且不能用下标形式添加元素

vector对象(以及string对象)的下标运算符可用于访问已经存在的元素,而不能用于添加元素。

使用下标访问一个不存在的元素将引发错误,这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。缓冲区溢出指的就是这类错误。

3.4 迭代器介绍

类似于指针类型,迭代器也提供了对对象的间接访问。有效的迭代器指向某个元素,或者指向容器中尾元素的下一位置;其他所有情况都是无效的。

3.4.1 使用迭代器

迭代器的获取不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型拥有名为beginend的成员。

如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器,指向容器“尾元素的下一个位置”。

标准容器迭代器的运算符相关说明
*iter返回迭代器iter所指元素的引用
iter->mem解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter令iter指示容器中的下一个元素
--iter令iter指示容器中的上一个元素
iter1 == iter2判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等
iter1 != iter2

和指针类似,可以使用解引用迭代器来获取它所指向的元素,执行解引用的迭代器必须合法并确实指向某个元素。解引用一个非法迭代器或尾后迭代器都是未定义的行为。

因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增解引用的操作。

使用迭代器的for循环常用的是 != ,原因和大多数人更愿意使用迭代器而非下标一样:这种编程风格在标准库提供的所有容器上都有效。所有标准库容器上都定义了 == 和 !=,但是大多数都没有定义 <。

拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型:

vector<int>::iterator it;            //it能读写vector<int>的元素
string::interator it2;                //it2能读写string对象中的字符

vector<int>::const_iterator it3;    //it3只能读元素,不能写元素
string::const_interator it4;        //it4只能读字符,不能写字符

如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既可以使用iterator也能使用const_iterator。

begin和end返回的具体类型由对象是否常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。

C++11新标准引入了两个新函数:cbegin和cend,便于专门得到const_iterator类型的返回值。

vector对象能够动态增长,其某些操作会使迭代器失效,因此有些限制:

  • 不能在范围for循环中向vector对象添加元素
  • 任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效

3.4.2 迭代器运算

vector和string迭代器支持的运算相关说明
iter + n迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来性比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置
iter - n迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来性比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置
iter1 += n迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1
iter1 -= n迭代器加法的复合赋值语句,将iter1减n的结果赋给iter1
iter1 - iter2两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向同一个容器中的元素或者尾元素的下一位置
>,>=,<,<=迭代器的关系运算符,如果某迭代器所指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向同一个容器中的元素或者尾元素的下一位置

两个迭代器相减的结果的值类型是difference_type,是带符号整型数。string和vector都定义了这个类型。

3.5 数组

数组是一种复合类型,声明形如a[d],其中a是数组的名字,d是数组的维度(必须大于0,必须是一个常量表达式)。

3.5.1 定义和初始化内置数组

unsigned cnt = 42;            //不是常量表达式
cosntexpr unsigned sz = 42;    //常量表达式
int arr[10];                //含有10个整数的数组
int *parr[sz];                //含有42个整型指针的数组
string bad[cnt];            //错误:cnt不是常量表达式
string strs[get_size()];    //当get_size()是constexpr时正确,否则错误

和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值

定义数组时必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。

可以对数组的元素进行列表初始化,此时允许忽略数组的维度:

  • 如果在声明时没有指明维度,编译器根据初始值的数量计算并推测出来
  • 指明了维度,那么初始值的总数量不应该超出指定的大小,如果维度比初始值数量大,则提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值

字符数组有一种额外的初始化形式:使用字符串字面值对数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符会像字符串的其他字符一样被拷贝到字符数组中去:

char a1[] = {'c', '+', '+'};        //列表初始化,没有空字符
char a2[] = {'c', '+', '+', '\0'};    //列表初始化,含有显示的空字符
char a3[] = "C++";                //自动添加表示字符串结束的空字符
const char a4[6] = "Daniel";    //错误:没有空间可存放空字符!

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:

int a[] = {0,1,2};    //含有3个整数的数组
int a2[] = a;        //错误:不允许使用一个数组初始化另一个数组
a2 = a;                //错误:不能把一个数组直接赋值给另一个数组

复杂的数组声明:

int *ptrs[10];        //ptrs是含有10个整型指针的数组
int &refs[10] = /* ? */;    //错误:不存在引用的数组
int (*Parray)[10] = &arr;    //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr;    //arrRef引用一个含有10个整数的数组
int *(&array)[10] = ptrs;    //array引用的对象是一个含有10个int型指针的数组

类型修饰符从右向左绑定,有小括号的话则是从内向外。Parray是个指向大小为10的数组的指针,arrRef引用的对象是一个大小为10的整型数组。

3.5.2 访问数组元素

数组元素能够使用范围for语句或下标运算符来访问。使用数组下标时,通常将其定义为size_t类型(一种机器相关的无符号类型),它被设计得足够大以便能表示内存中任意对象的大小,定义在cstddef头文件中。

数组的下标也要进行严格检查,合理的下标应该大于等于0而且小于数组的大小。

大多数常见的安全问题都源于缓冲区溢出错误,当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此错误。

3.5.3 指针和数组

通常情况,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组的元素使用取地址符就能得到指向该元素的指针:

string nums[] = {"one", "two", "three"};
string *p = &nums[0];    //p指向nums的第一个元素
string *p2 = nums;        //等价p2 = &nums[0]

数组有一个特性:在很多用的数组名的地方,编译器会自动地将其替换为一个指向数组首元素的指针。

int ia[] = {0,1,2,3,4,5};
auto ia2(ia);    //ia2是一个整型指针,指向ia的第一个元素
auto ia2(&ia[0]);    //等价上面语句,ia会自动转换成&ia[0]
ia2 = 42;        //错误:ia2是一个指针,不能用int值给指针赋值

当使用decltype关键字时,上诉转换不会发生,decltype(ia)返回的类型是整数数组:

decltype(ia) ia3 = {0,1,2,3,4,5};
ia3 = p;    //错误,不能用整型指针给数组赋值
ia3[4] = i;    //正确:把i的值赋给i3的一个元素

指向数组元素的指针具有更多功能,vector和string的迭代器支持的运算,数组指针都支持。通过数组的名字或首元素地址可以得到指向首元素的指针,数组的尾指针则需要设法获取:

int *e = &arr[10];    //指向arr尾元素的下意味着的指针

不能对尾指针指向解引用或递增的操作。

C++11新标准引入了两个名为begin和end的函数,更简单、安全的获取数组的首尾指针。与容器的成员函数同名但是不是成员函数,使用形式是将数组作为它们的参数:

int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia);    //指向ia首元素的指针
int *last = end(ia);    //指向arr尾元素的下一位置的指针

两个指针相减的结果是它们之间的距离,是一种名为prtdiff_t的标准库类型,和size_t一样,prtdiff_t也是一种定义在cstddef头文件中的机器相关的类型。因为差值可能为负数,所以它是一种带符号类型。

只有两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,才能用关系运算符进行比较。如果两个指针指向不相关的对象,则不能比较它们。

指针运算同样适用于空指针和所指对象并非数组的指针,后一种情况下,两个指针必须指向同一个对象或该对象的下一位置。如果p是空指针,允许给p加上或减去一个值为0的整型常量表达式。两个空指针相减,结果是0。

int ia[] = {0,2,4,6,8};
int i = ia[2];
int *p = ia;
i = *(p + 2);    //等价于i = ia[2]
int *p = &ia[2];    //p指向索引为2的元素
int j = p[1];        //等价于*(p + 1),就是ia[3]表示的那个元素
int k = p[-2];        //p[-2]是ia[0]表示的那个元素

标准库类型(string、vector等)限定使用的下标必须是无符号类型,而内置的下标运算无此要求,但是地址必须指向原来的指针所指同一数组中的元素。

3.5.4 C风格字符串

字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(‘\0’)

C风格字符串的函数(不负责验证字符串参数)函数相关说明
strlen(p)返回p的长度,空字符不计算在内
strcmp(p1, p2)比较p1和p2的相等性。如果p1==p2,返回0;如果p1 > p2,返回一个正值;如果p1 < p2,返回一个负值
strcat(p1, p2)将p2附加到p1之后,返回p1
strcpy(p1, p2)将p2拷贝给p1,返回p1

传入此类函数的指针必须指向以空字符串作为结束的数组

char ca[] = {'C', '+', '+'};    //不以空字符结束
cout << strlen(ca) << endl;        //严重错误:ca没有以空字符结束

比较标准库string对象时,用的是普通的关系运算符和相等性运算符:

string s1 = "A string example";
string s2 = "A different string";
if(s1 < s2)            //false:s2小于s1

如果把这些运算用在两个C风格字符串上,实际比较的将是指针而非字符串本身:

const char ca1[] = "A string example";
const char ca2[] = "A differenet string";
if (ca1 < ca2)        //未定义的:试图比较两个无关地址
if (strcmp(ca1, ca2) < 0)    //和两个string对象的比较s1 < s2 效果一样

连接或拷贝C风格字符串也与标准库string对象的同样操作差别很大:

string largeStr = s1 + " " + s2;
const char ca3 = ca1 + ca2;        //错误:试图将两个指针相加。无意义,非法。

//正确方法是使用strcat函数和strcpy函数,而且必须提供一个存放结果字符串的数组,
//该数组必须足够大,以便容纳字符串以及末尾的空字符
strcpy(largeStr, ca1);
strcat(largeStr, " ");
strcat(largeStr, ca2);

largeStr数组大小计算错误将引发严重错误,我们在故事largeStr所需的空间时不容易估算准确,而且largeStr所存的内容一旦改变,就必须重新检查其空间是否足够。

使用标准库string要比使用C风格字符串更安全、更高效。

3.5.5 与旧代码的接口

很多C++程序在标准库出现之前就已经写成了,还有一些C++程序实际上是C语言或其它语言的接口程序,它们都没有用C++标准库,因此现代C++程序必须与充满了字符数组或C风格字符串的代码衔接。

允许使用字符串字面值来初始化string对象,更一般的情况是,任何出现字符串字面值的地方都可以用空字符结束的字符数组来替代:

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值
  • 在string对象的加法中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是):在string对象的复合赋值运算(+=等)中允许使用以空字符结束的字符数组作为右侧的运算对象

上述性质反过来不成立:

string s("Hello world");    //s的内容是Hello World
char *str = s;                //错误:不能使用string对象来初始化char*
const char *str = s.c_str();    //正确:string对象通过函数初始化指向字符的指针

结果指针的类型是const char*,从而确保我们不会改变字符数组的内容。我们无法保证c_str函数返回的数组一直有效,事实上,如果后续操作改变了s的值可能让之前返回的数组失去作用。如果执行完c_str函数后程序相一直能使用其返回的数组,最好将该数组重新拷贝一份。

不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反的,允许使用数组来初始化vector对象,只需要指明要拷贝区域的首元素地址和尾后元素地址就可以了:

int int_arr[] = {0,1,2,3,4,5};
vector<int> ivec(begin(int_arr), end(int_arr));
vector<int> subVec(int_arr + 1, int_arr+4);
//int_arr[1]、int_arr[2]、int_arr[3]三个元素

现代的C++程序应该尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串

3.6 多维数组

严格来说,C++没有多维数组,通常所说的多维数组其实是数组的数组。

当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另一个维度表示其元素(也是数组)大小:

int ia[3][4];    //大小为3的数组,每个元素是含有4个整数的数组
//大小为10的数组,它的每个元素都是大小为20的数组
/这些数组的元素是含有30个整数的数组
int arr[10][20][30] = {0};    //将所有元素初始化为0

按照由内而外的顺序阅读此类定义有助于更好的理解真实含义。

对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

允许使用花括号括起来的一组值初始化多维数组:

int ia[3][4] = {    //三个元素,每个元素都是大小为4的数组
    {0,1,2,3},        //第1行的初始值
    {4,5,6,7},        //第2行的初始值
    {8,9,10,11}        //第3行的初始值
};
//内层嵌套的花括号并非必须的
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};    //与上面等价

//初始化多维数组,并非所有元素的值都必须包含在初始化列表之内
int ia[3][4] = {{0}, {4}, {8}};    //未列出的元素执行默认初始化
//这时候省略内层花括号的话,结果大不一样
int ix[3][4] = {0, 3, 6, 9};    //初始化的是第1行的4个元素,其他元素默认初始化为0

如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果是给定类型的元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组:

ia[2][3] = arr[0][0][0];    //用arr的首元素为ia最后一行的最后一个元素赋值
int (&row)[4] = ia[1];        //把row绑定到ia的第二个4元素数组上

程序中经常使用两层嵌套的for循环来处理多维数组的元素:

constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
for(size_t i = 0; i != rowCnt; ++i){
    for(size_t j = 0; j != colCnt; ++j){
        ia[i][j] = i * colCnt + j;
    }
}

C++11新标准引入的范围for循环可以用于处理多维数组:

size_t cnt = 0;
for(auto &row : ia){
    for(auto &col : row){
        col = cnt;
        ++cnt;
    }
}

使用引用,是为了避免数组被自动转换成指针:

for(auto row : ia)
    for(auto col : row)

因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。这样row是int *类型,放在内层循环就不合法了。

因此要用范围for语句处理多维数组,除了内层的循环外,其他所有循环的控制变量都应该是引用类型。

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。

int ia[3][4];
int (*p)[4] = ia;    //p指向含有4个整数的数组
int *p[4];            //整型指针的数组
p = &ia[2];            //p指向ia的尾元素

C++11新标准提出,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型了:

//p指向含有四个整数的数组
for(auto p = ia; p != ia + 3; ++p){
    //q指向4个整数数组的首元素,即q指向一个整数
    for(auto q = *p; q != *p + 4; ++q)
        cout << *q << ' ';
    cout << endl;
}

for(auto p = begin(ia); p != end(ia); ++p){
    for(auto q = begin(*p); q != end(*q); ++q)
        cout << *q << ' ';
    cout << endl;
}

读、写和理解一个指向多维数组的指针是一个让人不胜其烦的工作,使用类型别名简化多维数组的指针:

using int_array = int[4];
typedef int int_array[4];    //等价的typedef声明
for(int_array *p = ia; p != ia +3; ++p){
    for(int *q = *p; q != *p + 4; ++q)
        cout << *q << ' ';
    cout << endl;
}